package ph.com.gs3.chat.communicators;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;

import ph.com.gs3.chat.communicators.HttpCommunicatorModule.CommunicatorException;

public class HttpCommunicator {

	public static final String TAG = HttpCommunicator.class.getSimpleName();

	private static final char PARAMETER_DELIMITER = '&';
	private static final char PARAMETER_EQUALS_CHAR = '=';

	public static final int STATUS_INVALID_URL = -2;
	public static final int STATUS_DISCONNECTED = -1;
	public static final int STATUS_ERROR_ON_CONNECT = 0;
	public static final int STATUS_CONNECTED = 1;

	/**
	 * This is how long (in milliseconds) will the application wait until the application
	 * is able to connect to the server being checked.
	 */
	public static final int CHECK_CONNECTION_TIMEOUT = 10000;	// 10s

	private int readTimeout = 0;
	private int connectionTimeout = 0;

	/**
	 * Maintains session in cookie in the whole application
	 */
	private static CookieManager cookieManager = new CookieManager();

	/**
	 * Checks connection to the specified URL.
	 * 
	 * @param urlString
	 *            the url to test connection to.
	 * @return an integer constant that can either be Communicator.STATUS_INVALID_URL,
	 *         Communicator.STATUS_DISCONNECTED, Communicator.STATUS_ERROR_ON_CONNECT, or
	 *         Communicator.STATUS_CONNECTED.
	 */
	public int testConnection(String urlString) {

		if (urlString == null || urlString.isEmpty()) {
			throw new IllegalStateException("Tried to test connection with an empty server.");
		}
		
		URL url;
		try {
			url = new URL(urlString);
		} catch (MalformedURLException e) {
			return STATUS_INVALID_URL;
		}

		HttpURLConnection conn;
		try {
			conn = (HttpURLConnection) url.openConnection();
			conn.setConnectTimeout(CHECK_CONNECTION_TIMEOUT);
			conn.connect();

			if (HttpURLConnection.HTTP_OK == conn.getResponseCode()) {
				return STATUS_CONNECTED;
			} else {
				return STATUS_ERROR_ON_CONNECT;
			}

		} catch (IOException e) {
			return STATUS_DISCONNECTED;
		}
	}

	/**
	 * Utility method for getResponseString that sets the request method to GET as
	 * default. See {@link #getResponseString(String, Map, String) getResponseString}
	 * method
	 */
	public String getResponseString(String urlString, Map<String, String> parameters)
	        throws CommunicatorException {
		return getResponseString(urlString, parameters, "GET");
	}

	/**
	 * Utility method for getResponseString that sets no parameters and the request method
	 * to GET as default. See {@link #getResponseString(String, Map, String)
	 * getResponseString} method
	 */
	public String getResponseString(String urlString) throws CommunicatorException {
		return getResponseString(urlString, null, "GET");
	}

	/**
	 * Gets the response from the url specified, the user may specify parameters and the
	 * request method. Additionally, the user may set the readTimeout and/or
	 * connectionTimout before calling this method.
	 * 
	 * <pre>
	 * 	{@code
	 * 		communicatorInstance.readTimeout = 1000	//	milliseconds
	 * 		communicatorInstance.connectionTimeout = 3000	//	milliseconds
	 * 		String response = communicatorInstance.getResponseString("http://my.api.com", null, "GET");
	 * 	}
	 * </pre>
	 * 
	 * @param urlString
	 *            the URL of the request to be made.
	 * @param parameters
	 *            a Map<String, String> object representation of the parameters
	 * @param requestMethod
	 *            the request method to use, (GET, POST, PUT, DELETE)
	 * @return a string containing the response from the request
	 * @throws IOException
	 */
	public String getResponseString(String urlString, Map<String, String> parameters,
	        String requestMethod) throws CommunicatorException {

		CookieHandler.setDefault(HttpCommunicator.cookieManager);

		URL url;

		try {
			url = new URL(urlString);
		} catch (MalformedURLException e) {
			e.printStackTrace();
			return null;
		}

		HttpURLConnection conn = null;

		try {

			try {
				conn = (HttpURLConnection) url.openConnection();
			} catch (IOException e) {
				throw new CommunicatorException("Failed to open connection to URL " + urlString
				        + ". Unexpected IOException: " + e.getMessage(), e);
			}

			if (readTimeout > 0) {
				conn.setReadTimeout(readTimeout);
			}

			if (connectionTimeout > 0) {
				conn.setConnectTimeout(connectionTimeout);
			}

			try {
				conn.setRequestMethod(requestMethod);
			} catch (ProtocolException e) {
				throw new CommunicatorException("Failed to set request method to " + requestMethod,
				        e);
			}

			conn.setDoInput(true);

			if (parameters != null) {

				// send the parameters out
				PrintWriter out;
				try {
					out = new PrintWriter(conn.getOutputStream());
					out.print(createQueryString(parameters));
					out.close();
				} catch (IOException e) {

					// check if the server is simply unreachable
					if (e.getMessage().indexOf("EHOSTUNREACH") >= 0) {
						throw new CommunicatorException("Host " + urlString
						        + " is currently unreachable, please contact your administrator.",
						        e);
					} else {
						throw new CommunicatorException(
						        "Failed to get output stream from existing connection to URL "
						                + urlString + ". Unexpected IOException: " + e.getMessage(),
						        e);
					}
				}

			}

			// handle issues
			int statusCode;
			try {
				statusCode = conn.getResponseCode();
				if (statusCode != HttpURLConnection.HTTP_OK) {
					throw new CommunicatorException(
					        "Server status returned is not 200 (OK), contact your administrator");
				}
			} catch (IOException e) {
				throw new CommunicatorException("Unable to retrieve status code of the response. "
				        + e.getMessage(), e);
			}

			// convert the response to string and then return it
			try {
				return parseInputStream(conn.getInputStream());
			} catch (UnsupportedEncodingException e) {
				throw new CommunicatorException("This device does not support UTF-8 encoding.", e);
			} catch (IOException e) {
				throw new CommunicatorException(
				        "Unexpected IOException when trying to read server response, exception: "
				                + e.getMessage(), e);
			}

		} finally {
			if (conn != null) {
				conn.disconnect();
			}
		}

	}

	/**
	 * Converts a given map (key value pair) of strings to a query string ready to be sent
	 * to the server as request parameters.
	 * 
	 * @param parameters
	 *            the Map of values to become parameters
	 * @return a string representation of the parameters
	 */
	public static String createQueryString(Map<String, String> parameters) {
		StringBuilder parametersAsQueryString = new StringBuilder();
		if (parameters != null) {
			boolean firstParameter = true;

			for (String parameterName : parameters.keySet()) {
				if (!firstParameter) {
					parametersAsQueryString.append(PARAMETER_DELIMITER);
				}

				try {
					parametersAsQueryString.append(parameterName).append(PARAMETER_EQUALS_CHAR)
					        .append(URLEncoder.encode(parameters.get(parameterName), "UTF-8"));
				} catch (UnsupportedEncodingException e) {
					/**
					 * TODO: add logging capabilities for runtime errors like this
					 */
					throw new RuntimeException("UTF-8 not supported by this device.");
				}

				firstParameter = false;
			}
		}
		return parametersAsQueryString.toString();
	}

	/**
	 * Reads a given InputStream object and returns a string from it.
	 * 
	 * @param stream
	 *            the input stream to convert
	 * @return The string format of the given input stream
	 * @throws IOException
	 * @throws UnsupportedEncodingException
	 */
	public static String parseInputStream(InputStream stream) throws IOException,
	        UnsupportedEncodingException {
		BufferedReader bufferedReader = null;
		StringBuilder sbuilder = new StringBuilder();

		String line;
		try {

			bufferedReader = new BufferedReader(new InputStreamReader(stream));
			while ((line = bufferedReader.readLine()) != null) {
				sbuilder.append(line);
			}

		} finally {
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}

		return sbuilder.toString();
	}

	// ===========================================================================
	// Getters & Setters

	public int getReadTimeout() {
		return readTimeout;
	}

	public void setReadTimeout(int readTimeout) {
		this.readTimeout = readTimeout;
	}

	public int getConnectionTimeout() {
		return connectionTimeout;
	}

	public void setConnectionTimeout(int connectionTimeout) {
		this.connectionTimeout = connectionTimeout;
	}

}
